WebGL 索引组网与面剔除
一句话结论:索引决定网格怎么拼,缠绕顺序决定哪一面朝外,面剔除据此丢弃背面三角形以省掉一半绘制量。
索引组网与顶点缠绕顺序
是什么
索引组网是用索引数组描述三角形由哪些顶点组成的方式,缠绕顺序是这三个顶点在索引中的排列方向。WebGL 默认把屏幕空间下逆时针(CCW)排列的三角形判定为正面。
解决什么问题
它解决网格拓扑的表达与正反面判定问题。索引让顶点可以共享,避免重复存储;缠绕顺序则给每个面定义了法向,使背面剔除、双面材质与法线计算都有依据。
为什么这样做
三维网格中一个顶点通常被多个三角形共用,用索引表达拓扑可以把顶点数降到接近网格点数。而正反面判定需要一致的规则,采用投影后的有向面积符号判断既简单又可由硬件并行完成,因此成为图形 API 的通用约定。
怎么用
// 矩形:4 个顶点,逆时针缠绕表示正面朝向观察者
const positions = new Float32Array([
-0.5, -0.5, 0, // 0 左下
0.5, -0.5, 0, // 1 右下
0.5, 0.5, 0, // 2 右上
-0.5, 0.5, 0, // 3 左上
]);
const indices = new Uint16Array([
0, 1, 2, // 逆时针
0, 2, 3,
]);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_SHORT, 0);
计算方式:把三个顶点投影到屏幕,用叉积的 z 分量判断方向,正值通常对应逆时针。
常见坑
- 索引顺序写反导致面被剔除 → 原因:缠绕方向与默认 CCW 相反 → 处理:调整索引顺序或设置 frontFace(CW)
- 顶点共享后法线被平均 → 原因:平滑着色与硬边冲突 → 处理:硬边处拆分顶点
- 索引超出顶点范围 → 原因:索引从 0 起算 → 处理:最大索引等于顶点数减一
- 网格存在孤立三角形 → 原因:拓扑拼接遗漏 → 处理:生成时按行列统一构造索引
面剔除与可见性控制
是什么
面剔除是开启 CULL_FACE 后由 GPU 丢弃背面三角形的功能,配合 cullFace(剔除正面、背面或双面)与 frontFace(正面判定方向)共同控制哪些面参与绘制。
解决什么问题
它解决封闭物体背面被无意义绘制的问题。立方体、角色这类封闭网格的背面必然被前面遮挡,若照常光栅化,会造成大量无效的片元着色与深度写入。
为什么这样做
背面剔除发生在图元装配之后、光栅化之前,几乎零成本就能丢弃约一半三角形。相比深度测试的"先画再丢弃",剔除直接避免了后续所有计算,是性价比最高的几何阶段优化之一。
怎么用
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.BACK); // 丢弃背面(默认)
gl.frontFace(gl.CCW); // 逆时针为正面(默认)
// 需要看到内壁时改为剔除正面,或临时关闭
gl.cullFace(gl.FRONT);
gl.disable(gl.CULL_FACE);
| 设置 | 行为 |
|---|---|
| CULL_FACE + BACK | 丢弃背面,最常用 |
| CULL_FACE + FRONT | 丢弃正面,用于看内壁 |
| FRONT_AND_BACK | 全部丢弃,仅用于生成阴影图等特殊场景 |
| 未开启 CULL_FACE | 正反面都绘制 |
常见坑
- 开启后模型整体消失 → 原因:缠绕顺序反了 → 处理:先关剔除确认模型,再修正索引或 frontFace
- 平面图形被剔除 → 原因:单面片没有厚度 → 处理:平面类物体关闭剔除或使用双面材质
- 镜像变换后正反面颠倒 → 原因:负缩放翻转了缠绕方向 → 处理:变换后重设 frontFace 或避免负缩放
- 透明物体内部看不到 → 原因:剔除掉了背面 → 处理:两趟绘制背正面并排序
要点总结
- 索引描述拓扑,缠绕顺序描述朝向,默认逆时针为正面
- 索引从 0 起算,最大索引等于顶点数减一
- 面剔除几乎零成本,可省掉封闭网格约一半的绘制量
- 模型消失先关剔除排查,再修索引方向或 frontFace
- 平面与透明物体需要关闭剔除或分两趟绘制
